package com.ctsi.commons.util;

import com.ctsi.commons.util.cache.CommUtilCache;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.script.*;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;

/**
 * js执行 
 * 
 * @author yueming
 * 
 */
public class JsUtil {
	private static final Logger log = LoggerFactory.getLogger(JsUtil.class);

	// scriptExit

	private static final ScriptEngineManager manager = new ScriptEngineManager();
	static {
		manager.put("scriptExit", new ScriptEarlyExit());
	}

	public static ScriptContext createScriptContext(Map<String, Object> context) {
		if (context == null) {
			context = new HashMap<String, Object>();
		}
		context.put("jscontext", context);
		ScriptContext scriptContext = new SimpleScriptContext();

		Bindings bindings = new SimpleBindings(context);
		scriptContext.setBindings(bindings, ScriptContext.ENGINE_SCOPE);
		return scriptContext;
	}
	
	public static Bindings  createBindings (ScriptEngine engine,Map<String, Object> context) {
		if (context == null) {
			context = new HashMap<String, Object>();
		}
		context.put("jscontext", context);
		Bindings bindings = engine.getBindings(ScriptContext.ENGINE_SCOPE);
		
		bindings.putAll(context);
 
 
		return bindings;
	}
    /**
     * 先编译执行，否则解释执行
     * @param language
     * @param script
     * @param context
     * @return
     * @throws Exception
     */
	public static Object evaluate(final String language,final  String script, Map<String, Object> context) {
        if(UtilValidate.isEmpty(script)){
        	log.warn("script Evaluation error. Empty script");
        	return null;
        }
		try {
			CompiledScript compiledScript = compileScriptString(language, script);
			if (compiledScript != null) {
				return compiledScript.eval(createScriptContext(context));

			}

			ScriptEngine engine = manager.getEngineByName(language);
			if (engine == null) {
				throw new IllegalArgumentException("The script type is not supported for language: " + language);
			}

			ScriptContext scriptContext = createScriptContext(context);
			return engine.eval(script, scriptContext);
		} catch (Exception e) {
			String errMsg = "Error running " + language + " script [" + script + "]: " + e.toString();
			log.error(errMsg, e);
			throw new IllegalArgumentException(errMsg);
		}
	}

	/**
	 * 执行js文件
	 * 
	 * @param url
	 * @param scriptContext
	 * @return
	 * @throws ScriptException
	 * @throws NoSuchMethodException
	 * @throws IOException
	 */
	public static Object executeScript(URL url, ScriptContext scriptContext) throws ScriptException, IOException {
		String filePath = url.toString();

		CompiledScript script = compileScriptFile(url);
		if (script != null) {
			return script.eval(scriptContext);
		}

		String fileExtension = UtilFile.getFileType(filePath);
		ScriptEngine engine = manager.getEngineByExtension(fileExtension);
		if (engine == null) {
			throw new IllegalArgumentException("The script type is not supported for location: " + filePath);
		}

		log.debug("Begin processing script [ {} ] using engine {} ", filePath, engine.getClass().getName());

		engine.setContext(scriptContext);

		
		try(BufferedReader reader = new BufferedReader(new InputStreamReader(url.openStream(), StandardCharsets.UTF_8))){
			Object result = engine.eval(reader);
			return result;
		}
		
		 
		
	}

	/**
	 * 执行文件中的函数,目前编译脚本执行函数需要用Bindings，ScriptContext不行
	 * 
	 * @param url
	 * @param functionName
	 * @param scriptContext
	 * @param args
	 * @return
	 * @throws ScriptException
	 * @throws NoSuchMethodException
	 * @throws IOException
	 */
	public static Object executeScript(URL url, String functionName, ScriptContext scriptContext, Object[] args) throws ScriptException, NoSuchMethodException, IOException {
		String filePath = url.toString();

		String fileExtension = UtilFile.getFileType(filePath);
		ScriptEngine engine = manager.getEngineByExtension(fileExtension);
		if (engine == null) {
			throw new IllegalArgumentException("The script type is not supported for location: " + filePath);
		}

		log.debug("Begin processing script [ {} ] using engine {} ", filePath, engine.getClass().getName());

		engine.setContext(scriptContext);

		
		Object result ;
	 
		try(BufferedReader reader = new BufferedReader(new InputStreamReader(url.openStream(), StandardCharsets.UTF_8))){
			result = engine.eval(reader);	
		}
		if (UtilValidate.isNotEmpty(functionName)) {
			try {
				Invocable invocableEngine = (Invocable) engine;
				result = invocableEngine.invokeFunction(functionName, args == null ? UtilObject.emptyObjectArray : args);
			} catch (ClassCastException e) {
				throw new ScriptException("Script engine " + engine.getClass().getName() + " does not support function/method invocations");
			}
		}
		return result;
	}

	// 目前编译脚本执行函数,需要用Bindings
	public static final  Object executeScript(final String language,final  String script,final  String functionName, Object[] args) throws ScriptException, NoSuchMethodException {

		CompiledScript cs=compileScriptString(language,script);
		if(cs!=null){
			
			return executeScript(cs,functionName,createBindings(cs.getEngine(),null),args);	
		}
		
		ScriptEngine engine = manager.getEngineByName(language);
		if (engine == null) {
			throw new IllegalArgumentException("The script type is not supported for language: " + language);
		}

		log.debug("Invoking function/method {}", functionName);

		try {

			engine.eval(script);
			Invocable invocableEngine = (Invocable) engine;

			return invocableEngine.invokeFunction(functionName, args == null ? UtilObject.emptyObjectArray : args);
		} catch (ClassCastException e) {
			throw new ScriptException("Script engine " + engine.getClass().getName() + " does not support function/method invocations");
		}

	}

	public static Object executeScript(final String script,final  String functionName, Object[] args) throws ScriptException, NoSuchMethodException {
		return executeScript("JavaScript", script, functionName, args);

	}
	
	/**
	 * 
	 * @param script
	 * @param functionName
	 * @param scriptContext
	 * @param args
	 * @return
	 * @throws ScriptException
	 * @throws NoSuchMethodException
	 */
    public static Object executeScript(CompiledScript script,final  String functionName, Bindings  scriptContext, Object[] args) throws ScriptException, NoSuchMethodException {
     
        Object result = script.eval(scriptContext);
        if (UtilValidate.isNotEmpty(functionName)) {
        
            log.trace("Invoking function/method {}" , functionName);
            
            ScriptEngine engine = script.getEngine();
            try {
                Invocable invocableEngine = (Invocable) engine;
                result = invocableEngine.invokeFunction(functionName, args == null ? UtilObject.emptyObjectArray  : args);
            } catch (ClassCastException e) {
            	log.error("Invoking function/method {} error :{}" , functionName,e.toString());
                throw new ScriptException("Script engine " + engine.getClass().getName() + " does not support function/method invocations");
            }
        }
        return result;
    }
    
    
	public static final Object eval(final String expression, ScriptContext scriptContext) {
		if (UtilValidate.isEmpty(expression)) {
			log.warn("  Empty expression");
			return null;
		}

		try {
			CompiledScript compiledScript = compileScriptString("JavaScript", expression);
			if (compiledScript != null) {
				return compiledScript.eval(scriptContext);

			} else {
				ScriptEngine engine = manager.getEngineByName("JavaScript");
				if (engine == null) {
					throw new IllegalArgumentException("The script type is not supported for language: JavaScript");
				}

				return engine.eval(expression, scriptContext);
			}

		} catch (Exception e) {
			log.error("eval error", e);
			log.error("expression is:{}", expression);
			throw new RuntimeException(e);
		}
	}

	public static final Object evalScript(final String expression, Map<String, ?> context) {
		return eval(expression, (Map<String, Object>) context);
	}

	/**
	 * 执行Js脚本
	 * 
	 * @param expression
	 *            表达式
	 * @param context
	 *            执行的环境变量
	 * @return Object 执行结果
	 */
	public static final Object eval(final String expression, Map<String, Object> context) {

		try {
			return evaluate("JavaScript", expression, context);

		} catch (Exception e) {
			log.error("eval error", e);
			log.error("expression is:{}", expression);
			throw new RuntimeException(e);
		}

	}

	public static final Object eval(final String expression) {


		try {
			return evaluate("JavaScript", expression, null);

		} catch (Exception e) {
			log.error("eval error", e);
			log.error("expression is:{}", expression);
			throw new RuntimeException(e);
		}

	}

	// public static CommUtilCache<String, CompiledScript> parsedScripts;
	// private static CommUtilCache<String, CompiledScript> getParsedScripts() {
	// if (parsedScripts == null)
	// parsedScripts = new CommUtilCache<String,
	// CompiledScript>("script.ParsedCache");
	// return parsedScripts;
	//
	// }

	/**
	 * 执行js脚本
	 * 
	 * @param expression
	 *            表达式
	 * @param context
	 *            环境变量
	 * @return 执行结果
	 */
	public static final Object eval(Reader expression, Map<String, Object> context) {

		assert (expression != null);

		String script = null;
		try {

			script = UtilFile.readToString(expression);

			return evaluate("JavaScript", script, context);

		} catch (Exception e) {
			log.error("eval error", e);
			log.error("expression is:{}", script);
			throw new RuntimeException(e);
		}

	}

	// private static void iniContext(Map<String, ? extends Object> context, ScriptEngine engine) {
	//
	// if (context != null) {
	// for (Map.Entry<String, ? extends Object> me : context.entrySet()) {
	// engine.put(me.getKey(), me.getValue());
	// }
	// // 便于返回数据
	//
	// engine.put("jscontext", context);
	// } else {
	// log.info("context is null");
	// }
	//
	// }

	public static final CommUtilCache<String, CompiledScript> compiledScripts = new CommUtilCache<String, CompiledScript>("script.CompiledScript");
    /**
     * 如果可能，获取编译版本
     * @param language
     * @param script
     * @return
     * @throws ScriptException
     */
	public static CompiledScript compileScriptString(final String language,final  String script) throws ScriptException {

		final String cacheKey = StringUtil.concatString(language,"://",script);// language.concat("://").concat(script);
		CompiledScript compiledScript = compiledScripts.get(cacheKey);
		if (compiledScript == null) {

			ScriptEngine engine = manager.getEngineByName(language);
			if (engine == null) {
				throw new IllegalArgumentException("The script type is not supported for language: " + language);
			}

			if (engine instanceof Compilable) {
				Compilable compilableEngine = (Compilable) engine;
				compiledScript = compilableEngine.compile(script);

				compiledScripts.put(cacheKey, compiledScript);
				log.debug("Compiled script [{}] using engine {}", script, engine.getClass().getName());
			} else {
				log.warn("Script engine {} does not implement Compilable", engine.getClass().getName());
			}

		}
		return compiledScript;
	}
    /**
     * 编译js
     * @param url
     * @return
     * @throws ScriptException
     * @throws IOException
     */
	public static CompiledScript compileScriptFile(URL url) throws IOException {
		final String filePath = url.toString();
		CompiledScript script = compiledScripts.get(filePath);
		if (script == null) {
			ScriptEngine engine = manager.getEngineByExtension(UtilFile.getFileType(filePath));
			if (engine == null) {
				throw new IllegalArgumentException("The script type is not supported for location: " + filePath);
			}
			try {
				if (engine instanceof Compilable){
					Compilable compilableEngine = (Compilable) engine;
 
					try(BufferedReader reader = new BufferedReader(new InputStreamReader(url.openStream(), StandardCharsets.UTF_8))){
						script = compilableEngine.compile(reader);
					}
					log.debug("Compiled script {} using engine {}", filePath, engine.getClass().getName());					
				}else{
					log.warn("Script engine {} does not implement Compilable", engine.getClass().getName());
				}


			} catch (ScriptException e) {

				log.error("compile    " + url + " error");

			}
			if (script != null) {
				compiledScripts.put(filePath, script);
			}
		}
		return script;
	}

	private static class ScriptEarlyExitException extends Exception {
		public ScriptEarlyExitException(String msg) {
			super(msg);
		}
	}

	protected static class ScriptEarlyExit {
		public void noMessage() throws ScriptEarlyExitException {
			throw new ScriptEarlyExitException(null);
		}

		public void withMessage(String msg) throws ScriptEarlyExitException {
			throw new ScriptEarlyExitException(msg);
		}
	}

}
